iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

在學習了如何使用 List 來顯示家用品清單後,今天我們要更進一步,實作讓使用者能夠新增和刪除家用品項目的功能。

新增項目

我們首先來實作新增項目的功能。這裡我們會用到一個簡單的 TextField 讓使用者輸入項目名稱,並用一個 Button 來觸發新增操作。我們的目標 UI 如圖所示:

https://ooorito.com/wp-content/uploads/2024/08/%E7%A4%BA%E6%84%8F%E5%9C%96-566x1024.webp

可以看到,上方有兩個 TextField 和一個 Button 水平排列,下方則是我們昨天已經製作好的列表。接下來,我們先來修改一下佈局。還記得我們之前介紹過的 VStackHStack 嗎?如果忘記了,記得回去複習一下 Day4 的文章唷!

因為列表和輸入框是垂直排列的,所以我們需要先加入 VStack,將 List 包起來。如果不想手動輸入程式碼,也可以在 List 的位置按下滑鼠右鍵,然後選擇「Embed in VStack」,這樣就能快速完成這一步囉!

VStack {
    List(viewModel.items) { item in
        HStack {
            Text(item.name)
                .font(.headline)
            Spacer()
            Text("數量: \(item.quantity)")
                .font(.subheadline)
                .foregroundColor(.gray)
        }
        .padding(.vertical, 8)
    }
}

https://ooorito.com/wp-content/uploads/2024/08/List-1-530x1024.webp

你會發現,這時候 UI 預覽並沒有發生什麼變化,因為我們還沒有加上其他元件。還記得示意圖中的 TextFieldButton 是怎麼排列的嗎?沒錯,它們是水平排列的,所以我們需要使用 HStack。接下來,我們來加入 TextFieldButtonHStack 吧!

TextField

TextField 是 SwiftUI 中用來接受使用者文字輸入的元件,類似 UIKit 中的 UITextField。它允許使用者在 App 中輸入資料,並將這些資料綁定到某個變數,以便後續處理。TextField 是實現表單、搜尋框等功能的關鍵元件。

在建立 TextField 時,它的初始化方法接受型別 Binding<String> 的參數,所以我們必須傳入型別為 String 的變數並將其綁定。這樣當使用者輸入文字時,TextField 綁定的資料就會自動更新。我們來增加它綁定的值:

@State private var newItemName: String = ""
@State private var newItemQuantity: String = ""

參考資料:

接著,我們可以繼續完成排版:

VStack {
    HStack {
        TextField("輸入家用品名稱", text: $newItemName)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
        
        TextField("數量", text: $newItemQuantity)
            .keyboardType(.numberPad)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
        
        Button(action: {
            print("Button Click")
        }) {
            Text("新增")
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(8)
        }
    }
    List(viewModel.items) { item in
        HStack {
            Text(item.name)
                .font(.headline)
            Spacer()
            Text("數量: \(item.quantity)")
                .font(.subheadline)
                .foregroundColor(.gray)
        }
        .padding(.vertical, 8)
    }
}

https://ooorito.com/wp-content/uploads/2024/08/%E5%8A%A0%E5%85%A5HStack%E5%BE%8C-540x1024.webp

這時候,我們的排版和示意圖已經相當接近了,但還少了上方的標題列,這時候就要請出我們的好幫手— NavigationView 啦!

NavigationView

NavigationView 是 SwiftUI 中的一個容器元件,主要用來建立層次結構式的導航界面。與 UIKit 中的 UINavigationController 類似,可以讓使用者在多個畫面之間切換,並提供標題和返回按鈕等導覽列功能。

為了利用 NavigationView 管理多層頁面的切換,我們必須用 NavigationView 包住它管理的第一頁畫面,之後就可以切換到第二頁、第三頁。這時候可以呼叫 navigationTitle 這個方法來設定標題欄上的文字。因此,我們從 VStack 呼叫 navigationTitle,將標題設為「家用品清單」。

NavigationView {
    VStack {
        HStack {
            TextField("輸入家用品名稱", text: $newItemName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            TextField("數量", text: $newItemQuantity)
                .keyboardType(.numberPad)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: {
                print("Button Click")
            }) {
                Text("新增")
                    .padding(.horizontal, 12)
                    .padding(.vertical, 8)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        
        List(viewModel.items) { item in
            HStack {
                Text(item.name)
                Spacer()
                Text("數量: \(item.quantity)")
            }
        }
        .navigationTitle("家用品清單")
    }
}

https://ooorito.com/wp-content/uploads/2024/08/%E7%A4%BA%E6%84%8F%E5%9C%96-566x1024.webp

這樣一來,我們的 UI 就已經和示意圖完全一致了!

但這還沒完成,目前點擊 Button 只會在主控台印出「Button Click」。我們需要在 Buttonaction 裡面加入程式碼,讓它呼叫 ViewModel 中的 addItem 方法,這樣才能實現點擊按鈕新增項目的功能。讓我們來改寫一下 Button 的程式碼吧!

Button(action: {
    if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
        viewModel.addItem(name: newItemName, quantity: quantity)
        newItemName = ""
        newItemQuantity = ""
    }
}) {
    Text("新增")
        .padding(.horizontal, 12)
        .padding(.vertical, 8)
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(8)
}

這樣就大功告成啦!來看看我們完成的結果吧!

https://ooorito.com/wp-content/uploads/2024/08/%E6%96%B0%E5%A2%9E.gif

如果剛剛的步驟拆解不夠清楚,這裡提供完整的程式碼供參考唷!

struct ContentView: View {
    @StateObject var viewModel = ItemViewModel()
    @State private var newItemName: String = ""
    @State private var newItemQuantity: String = ""

    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("輸入家用品名稱", text: $newItemName)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    TextField("數量", text: $newItemQuantity)
                        .keyboardType(.numberPad)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    Button(action: {
                        if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
                            viewModel.addItem(name: newItemName, quantity: quantity)
                            newItemName = ""
                            newItemQuantity = ""
                        }
                    }) {
                        Text("新增")
                            .padding(.horizontal, 12)
                            .padding(.vertical, 8)
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                
                List(viewModel.items) { item in
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("數量: \(item.quantity)")
                    }
                }
                .navigationTitle("家用品清單")
            }
        }
    }
}

參考資料:

刪除項目

完成新增功能後,使用者就可以新增項目到列表中。但是,有時候難免會因為手誤而新增了錯誤的項目,這時候就需要有刪除功能了。所以,接下來我們就來實作刪除功能吧!

List 中,我們還可以很容易地實作刪除功能。在這裡我們使用 onDelete Modifier 來實現。

onDelete()

在 SwiftUI 中,onDelete() 是一個非常實用的 Modifier,用於在 List 中實現刪除行為。它允許使用者通過滑動操作來刪除列表中的特定項目。當配合 ForEach 使用時,onDelete() 可以自動處理刪除操作,並更新畫面。

使用 onDelete() 的基本語法如下:

.onDelete(perform: deleteItems)

其中,deleteItems 是一個函數,負責處理實際的刪除邏輯。這個修飾符需要與 ForEach 結合使用,讓每個列表項目都有唯一的識別碼,從而正確執行刪除操作。

改寫 List 以加入 onDelete()

在我們的原始程式碼中,List 直接接收了 viewModel.items,並為每個項目生成了對應的畫面。然而,為了在列表中實作刪除功能,我們需要對這段程式碼進行一些改寫,將其與 onDelete() Modifier 結合使用。

原始程式碼如下:

List(viewModel.items) { item in
    HStack {
        Text(item.name)
            .font(.headline)
        Spacer()
        Text("數量: \(item.quantity)")
            .font(.subheadline)
            .foregroundColor(.gray)
    }
    .padding(.vertical, 8)
}

這段程式碼雖然可以正常顯示列表,但無法直接支持刪除功能。為了讓使用者能夠刪除不需要的項目,我們需要將 List 的內容改寫為 ForEach 結構,並增加 onDelete() Modifier,如下所示:

List {
    ForEach(viewModel.items) { item in
        HStack {
            Text(item.name)
                .font(.headline)
            Spacer()
            Text("數量: \(item.quantity)")
                .font(.subheadline)
                .foregroundColor(.gray)
        }
        .padding(.vertical, 8)
    }
    .onDelete(perform: deleteItems)
}

參考資料:onDelete(perform:) | Apple Developer Documentation

實作刪除功能

接下來,我們需要實作 deleteItems 函數,以處理實際的刪除操作。以下是一個簡單的實作範例:

func deleteItems(at offsets: IndexSet) {
    viewModel.items.remove(atOffsets: offsets)
}

這個函數接收一個 IndexSet,表示要刪除的項目在列表中的索引。通過調用 remove(atOffsets:) 方法,我們可以從 viewModel.items 中刪除相應的項目,並自動更新畫面。

整合後的完整程式碼如下:

struct ContentView: View {
    @StateObject var viewModel = ItemViewModel()
    @State private var newItemName: String = ""
    @State private var newItemQuantity: String = ""

    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("輸入家用品名稱", text: $newItemName)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    TextField("數量", text: $newItemQuantity)
                        .keyboardType(.numberPad)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    Button(action: {
                        if let quantity = Int(newItemQuantity), !newItemName.isEmpty {
                            viewModel.addItem(name: newItemName, quantity: quantity)
                            newItemName = ""
                            newItemQuantity = ""
                        }
                    }) {
                        Text("新增")
                            .padding(.horizontal, 12)
                            .padding(.vertical, 8)
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                
                List {
                    ForEach(viewModel.items) { item in
                        HStack {
                            Text(item.name)
                            Spacer()
                            Text("數量: \(item.quantity)")
                        }
                    }
                    .onDelete(perform: deleteItems)
                }
                .navigationTitle("家用品清單")
            }
        }
    }
    
    private func deleteItems(at offsets: IndexSet) {
        viewModel.items.remove(atOffsets: offsets)
    }
}

https://ooorito.com/wp-content/uploads/2024/08/%E5%88%AA%E9%99%A4.gif

這樣一來,使用者就能夠在列表中滑動刪除不需要的項目,並且畫面會自動更新。

總結

今天我們成功讓家用品清單活了起來,使用者現在可以新增和刪除項目,不再是死氣沈沈的靜態列表。透過這些實作,我們也進一步加深了對 SwiftUI 中資料管理和 UI 更新的理解。

明天,我們將開始學習如何將這些資料儲存到本地的 Core Data 中,讓我們的 App 能夠持久保存這些家用品項目。明天見!


上一篇
Day 9: 使用 SwiftUI 的 List 顯示家用品清單
下一篇
Day 11: 將資料儲存到 Core Data
系列文
用 SwiftUI 掌控家庭日用品庫存12
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言